package ch.ethz.fcl.metrofont;

import static org.lwjgl.opengl.GL11.GL_LINEAR;
import static org.lwjgl.opengl.GL11.GL_RGBA;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MAG_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_MIN_FILTER;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_S;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_WRAP_T;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_BYTE;
import static org.lwjgl.opengl.GL11.glBindTexture;
import static org.lwjgl.opengl.GL11.glGenTextures;
import static org.lwjgl.opengl.GL11.glTexImage2D;
import static org.lwjgl.opengl.GL11.glTexParameteri;

import java.awt.Graphics;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.Hashtable;
import java.util.Vector;

import javax.imageio.ImageIO;

import org.lwjgl.BufferUtils;
import org.lwjgl.examples.spaceinvaders.Texture;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;

public class TextureAtlas {
	Vector<Node> nodes = null;
	private int width;
	private int height;
	private int depth;

	private ColorModel glAlphaColorModel;
	private ColorModel glColorModel;

	private BufferedImage image;
	private Region black;
	private Texture tex;

	private int textureId;

	private boolean atlasChanged = false;

	// private boolean uploaded = false;

	/* Larger atlas will make result more accurate */
	public TextureAtlas(int width, int height, int depth) {
		
		
		image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);

		glAlphaColorModel = new ComponentColorModel(
				ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8,
						8, 8 }, true, false, ComponentColorModel.TRANSLUCENT,
				DataBuffer.TYPE_BYTE);

		glColorModel = new ComponentColorModel(
				ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] { 8, 8,
						8, 0 }, false, false, ComponentColorModel.OPAQUE,
				DataBuffer.TYPE_BYTE);

		this.nodes = new Vector<Node>();
		this.width = width;
		this.height = height;
		this.depth = depth;

		Node node = new Node(0, 0, width);
		this.nodes.add(node);

		int n = 4;
		Region r = getRegion(n, n);
		this.black = new Region(r.x + 1, r.y + 1, r.width - 2, r.height - 2);

		textureId = createTextureID();
	}

	public Texture getTexture() {
		return this.tex;
	}

	public void upload() {
		if (image == null)
			return;

		if (!atlasChanged)
			return;

		int target = GL11.GL_TEXTURE_2D;

		tex = new Texture(target, textureId);

		glBindTexture(target, textureId);

		tex.setWidth(image.getWidth());
		tex.setHeight(image.getHeight());

		glTexParameteri(target, GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
		glTexParameteri(target, GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
		glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

		ByteBuffer textureBuffer = convertImageData(image, tex);

		// produce a texture from the byte buffer
		glTexImage2D(target, 0, GL_RGBA, get2Fold(image.getWidth()),
				get2Fold(image.getHeight()), 0, GL_RGBA, GL_UNSIGNED_BYTE,
				textureBuffer);

		atlasChanged = false;
	}

	private static int get2Fold(int fold) {
		int ret = 2;
		while (ret < fold) {
			ret *= 2;
		}
		return ret;
	}

	/**
	 * Convert the buffered image to a texture
	 * 
	 * @param bufferedImage
	 *            The image to convert to a texture
	 * @param texture
	 *            The texture to store the data into
	 * @return A buffer containing the data
	 */
	@SuppressWarnings("rawtypes")
	private ByteBuffer convertImageData(BufferedImage bufferedImage,
			Texture texture) {
		ByteBuffer imageBuffer;
		WritableRaster raster;
		BufferedImage texImage;

		int texWidth = bufferedImage.getWidth();
		int texHeight = bufferedImage.getHeight();

		texture.setTextureHeight(texWidth);
		texture.setTextureWidth(texHeight);

		// create a raster that can be used by OpenGL as a source
		// for a texture
		if (bufferedImage.getColorModel().hasAlpha()) {
			raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
					texWidth, texHeight, 4, null);
			texImage = new BufferedImage(glAlphaColorModel, raster, false,
					new Hashtable());
		} else {
			raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE,
					texWidth, texHeight, 3, null);
			texImage = new BufferedImage(glColorModel, raster, false,
					new Hashtable());
		}

		// copy the source image into the produced image
		Graphics g = texImage.getGraphics();
		g.drawImage(bufferedImage, 0, 0, null);

		// build a byte buffer from the temporary image
		// that be used by OpenGL to produce a texture.
		byte[] data = ((DataBufferByte) texImage.getRaster().getDataBuffer())
				.getData();

		imageBuffer = ByteBuffer.allocateDirect(data.length);
		imageBuffer.order(ByteOrder.nativeOrder());
		imageBuffer.put(data, 0, data.length);
		imageBuffer.flip();

		return imageBuffer;
	}

	public void setChanged() {
		atlasChanged = true;
		tex = null;
	}

	private int createTextureID() {
		IntBuffer textureIDBuffer = BufferUtils.createIntBuffer(1);
		glGenTextures(textureIDBuffer);
		return textureIDBuffer.get(0);
	}

	public void show(boolean show) {
		if (show) {
			LoadAndShow.showIcon(image);

			File f = new File("../MetroFont/font/default.png");

			System.out.println(f.getAbsolutePath());
			try {
				if (!f.exists()) {
					f.mkdirs();
					f.createNewFile();
				}
				ImageIO.write(image, "png", f);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public void setRegion(int minX, int minY, int width, int height,
			BufferedImage subimage) {
		image.getGraphics().drawImage(subimage, minX, minY, null, null);
		image.getGraphics().dispose();
	}

	public int fit(int index, int width, int height) {
		Node node = nodes.get(index);
		int x = node.x;
		int y = node.y;
		int width_left = width;
		int i = index;

		if (x + width > this.width)
			return -1;

		while (width_left > 0) {
			node = nodes.get(i);
			y = Math.max(y, node.y);
			if (y + height > this.height)
				return -1;
			width_left -= node.width;
			i++;
		}
		return y;
	}

	public void merge() {
		for (int i = 0; i < nodes.size() - 1; i++) {
			Node current = nodes.get(i);
			Node next = nodes.get(i + 1);
			if (current.y == next.y) {
				current.setWidth(current.width + next.width);
				nodes.remove(i + 1);
				i--;
			}
		}
	}

	public Region getRegion(int width, int height) {
		int y;
		int best_height = Integer.MAX_VALUE;
		int best_width = Integer.MAX_VALUE;
		int best_index = -1;
		Region region = new Region(0, 0, width, height);

		for (int i = 0; i < nodes.size(); i++) {
			y = fit(i, width, height);
			if (y >= 0) {
				Node node = nodes.get(i);
				if ((y + height < best_height)
						|| (y + height == best_height && node.width < best_width)) {
					best_height = y + height;
					best_index = i;
					best_width = node.width;
					region.setX(node.x);
					region.setY(y);
				}
			}
		}

		if (best_index == -1)
			return new Region(-1, -1, 0, 0);

		Node node = new Node(region.x, region.y + region.height, region.width);
		nodes.add(best_index, node);

		for (int i = best_index + 1; i < nodes.size(); i++) {
			Node current = nodes.get(i);
			Node prev = nodes.get(i - 1);
			if (current.x < (prev.x + prev.width)) {
				int shrink = prev.x + prev.width - current.x;
				current.setX(current.x + shrink);
				current.setWidth(current.width - shrink);
				if (current.width <= 0) {
					nodes.remove(i);
					i--;
				} else
					break;
			} else
				break;
		}
		merge();
		return region;
	}

	public int getWidth() {
		return this.width;
	}

	public int getHeight() {
		return this.height;
	}

	public int getDepth() {
		return this.depth;
	}

	public Region getBlackRegion() {
		return black;
	}

	public void clear() {
		image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		nodes.clear();
		Node node = new Node(0, 0, this.width);
		nodes.add(node);

		int n = 4;
		Region r = getRegion(n, n);
		this.black = new Region(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
	}

	public class Region {
		int x;
		int y;
		int width;
		int height;

		public Region(int x, int y, int width, int height) {
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
		}

		public void setX(int x) {
			this.x = x;
		}

		public void setY(int y) {
			this.y = y;
		}

		public void setWidth(int width) {
			this.width = width;
		}

		public void setHeight(int height) {
			this.height = height;
		}
	}

	class Node {
		int x;
		int y;
		int width;

		public Node(int x, int y, int width) {
			this.x = x;
			this.y = y;
			this.width = width;
		}

		public void setX(int x) {
			this.x = x;
		}

		public void setY(int y) {
			this.y = y;
		}

		public void setWidth(int width) {
			this.width = width;
		}
	}
}
